package org.ii.dataprovider.dapper;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Util {
	
	
  private static String userAgent = "Dapper SDK (Java5, v0.3)";
  private static String externalUserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.8.1) Gecko/20061010 Firefox/2.0";
  
  /**
   * Returns the contents, properly encoded, of the page at url
   * 
   * @param url The URL to fetch
   * @return The contents of the page 
   * @throws IOException
   */
  public static String urlGetContents(URL url) throws IOException {
    
    // for security purposes, this function will not accept urls that do not 
    // start with http:// or https://
    if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https"))
      throw new MalformedURLException("URL must start with http or https");
    
    URLConnection urlConnection;
    urlConnection = url.openConnection();

    // we need to set the user agent, since some websites are mean and don't like java
    if (url.getHost().endsWith("dapper.net"))
      urlConnection.addRequestProperty("User-Agent", Util.userAgent);
    else
      urlConnection.addRequestProperty("User-Agent", Util.externalUserAgent);      
    
    // we need to see if there is a "Location" header, in which case we have to redirect
    if (urlConnection.getHeaderField("Location") != null) {
      
    }
    
    // the process we undergo here is not so simple, but it is what
    // we have to do to determine the encoding and apply it properly:
    // first, we try to read it from the headers.  if that works, great.
    // otherwise, we scan for it in meta tags in the data, and if we find it,
    // we reset the stream to the beginning, and re-read the data.
    // (we use a buffered input stream so mark() and reset() work)
    
    InputStreamReader iSR = null;
    BufferedInputStream is = new BufferedInputStream(urlConnection.getInputStream());
    is.mark(20480);
    
    // determine encoding to use
    boolean foundValidEncoding = false;
    try {
      String encoding = null;
      if (urlConnection.getContentType() != null)
        encoding = urlConnection.getContentType().replaceAll(".*=", "");
      else
        throw new java.io.UnsupportedEncodingException();
      
      iSR = new InputStreamReader(is, encoding);
      
      // if we got here, we found a valid encoding
      foundValidEncoding = true;
    }
    catch (java.io.UnsupportedEncodingException e) {
      iSR = new InputStreamReader(is);
    }

    // read the input
    BufferedReader in = new BufferedReader(iSR);

    String inputLine;
    StringBuilder sBuffer = new StringBuilder();
    
    Pattern pat = Pattern.compile("<meta [^>]*charset=([^\"]+)", Pattern.CASE_INSENSITIVE);
    String newEncoding = null;
    while ((inputLine = in.readLine()) != null) {
      sBuffer.append(inputLine);
      
      if (!foundValidEncoding) {
        
        Matcher mat = pat.matcher(sBuffer.toString());
        if (mat.find()) {
          newEncoding = mat.group(1).trim();
          break;        
        }
        
      }
    }
    
    // we found a new encoding while we were scanning the document
    // and now we have to start all over
    if (newEncoding != null) {
      is.reset();
      sBuffer = new StringBuilder();
      
      try {
        in = new BufferedReader(new InputStreamReader(is, newEncoding));
      }
      catch (UnsupportedEncodingException e) {
        in = new BufferedReader(new InputStreamReader(is));
      }
      
      while ((inputLine = in.readLine()) != null) {
        sBuffer.append(inputLine);
      }
    }
    
    in.close();
    return sBuffer.toString();
  }

  /**
   * Performs an HTTP POST to a server on port 80
   *
   * @param host The hostname of the remote web server
   * @param path The path portion of the URL
   * @param data A map where the keys are the POST variable names, the
   *             values are the values for those variables
   *
   * @return The data resulting from the POST request
   * @throws IOException
   */
  public static String httpPost(String host, String path, Map<String,String>data, String encoding) throws IOException {
    return Util.httpPost(host, path, data, encoding, 80, 0);
  }
  
  /**
   * Performs an HTTP POST to a server on a specific port
   * 
   * @param host The hostname of the remote web server
   * @param path The path portion of the URL
   * @param data A map where the keys are the POST variable names, the
   *             values are the values for those variables
   *
   * @return The data resulting from the POST request
   * @throws IOException
   */
  public static String httpPost(String host, String path, Map<String,String>data, String encoding, int port) throws IOException {
    return Util.httpPost(host, path, data, encoding, port, 0);
  }
  
  private static String httpPost(String host, String path, Map<String,String>data, String encoding, int port, int recurseNumber) throws IOException {

    // Construct data (using the right encoding - this matters because some times we post HTML
    String keyEncoding = encoding;
    if (keyEncoding == null)
      keyEncoding = "UTF-8";
    // make sure the encoding is valid
    try {
      URLEncoder.encode("test",keyEncoding);
    }
    catch (UnsupportedEncodingException e) {
      keyEncoding = "UTF-8";
    }
    
    ArrayList<String> dataParts = new ArrayList<String>();
    for (String key: data.keySet()) {
      dataParts.add(URLEncoder.encode(key,keyEncoding)+"="+URLEncoder.encode(data.get(key),keyEncoding));
    }
    String dataString = Util.join("&", dataParts);    

    // Create a socket to the host
    InetAddress addr = InetAddress.getByName(host);
    Socket socket = new Socket(addr, port);

    // Send header
    BufferedWriter wr = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF8"));
    wr.write("POST "+path+" HTTP/1.0\r\n");
    wr.write("Host: "+host+"\r\n");
    wr.write("Content-Type: application/x-www-form-urlencoded"+(encoding != null ? ";charset="+encoding : "")+"\r\n");
    wr.write("Content-Length: "+dataString.length()+"\r\n");
    if (host.endsWith("dapper.net"))
      wr.write("User-Agent: "+Util.userAgent+"\r\n");
    else
      wr.write("User-Agent: "+Util.externalUserAgent+"\r\n");
    wr.write("Connection: close\r\n");
    wr.write("\r\n");

    // Send data
    wr.write(dataString);
    wr.flush();

    // we get the response from the server
    // we wrap it in a buffered input stream, so that we can reset the stream
    // to the beginning if we find an encoding along the way
    BufferedInputStream is = new BufferedInputStream(socket.getInputStream());
    is.mark(20480);
    BufferedReader rd = new BufferedReader(new InputStreamReader(is));

    // start reading the data, paying attention along the way to encodings
    // and to potential redirects
    
    // data reading settings
    String line;
    StringBuilder dataSB = new StringBuilder();
    StringBuilder headersSB = new StringBuilder();
    Boolean headersOver = false;
    
    // encoding settings
    boolean foundEncoding = false;
    boolean haveReset = false;
    String newEncoding = null;
    Pattern headerPat = Pattern.compile("content-type: .*=(.*)", Pattern.CASE_INSENSITIVE);
    Pattern bodyPat   = Pattern.compile("<meta [^>]*charset=([^\"]+)", Pattern.CASE_INSENSITIVE);
    
    // redirect settings
    Pattern locationPat = Pattern.compile("location: (.*)", Pattern.CASE_INSENSITIVE);
    
    while ((line = rd.readLine()) != null) {
      if (!headersOver && line.replaceAll("[\r\n\f]+","").equals("")) {
        headersOver = true;
      }
      
      if (!headersOver) {
        headersSB.append(line);

        if (!foundEncoding) {
          Matcher mat = headerPat.matcher(line);
          if (mat.find()) {
            newEncoding = mat.group(1);
            foundEncoding = true;
          }
        }
        
        // check for a redirection, but make sure not recurse infinitely
        if (recurseNumber < 5) {
          Matcher locMat = locationPat.matcher(line);
          if (locMat.find()) {
            String newHost = host;
            String newPath = locMat.group(1);
            if (newPath.startsWith("http://")) {
              URL newUrl = new URL(locMat.group(1));
              newHost = newUrl.getHost();
              newPath = newUrl.getPath()+"?"+newUrl.getQuery();
            }
            return Util.httpPost(newHost, newPath, data, encoding, port, recurseNumber+1);
          }
        }
      }
      else {
        dataSB.append(line);

        if (!foundEncoding) {
          Matcher mat = bodyPat.matcher(dataSB.toString());
          if (mat.find()) {
            newEncoding = mat.group(1);
            foundEncoding = true;
          }
        }
      }
      
      // if we have found an encoding, reset the stream to the beginning,
      // reinitialize the reader with the right encoding, clear out the buffers
      if (foundEncoding && !haveReset) {
        is.reset();
        headersOver = false;
        try {
          rd = new BufferedReader(new InputStreamReader(is, newEncoding.trim()));
        }
        catch (UnsupportedEncodingException e) {
          rd = new BufferedReader(new InputStreamReader(is));
        }
        dataSB = new StringBuilder();
        headersSB = new StringBuilder();  
        haveReset = true;
      }
    }
        
    wr.close();
    rd.close();
    
    return dataSB.toString();
  }

  /**
   * Convenience function to create a new string from multiple strings
   * 
   * @param separator
   * @param list
   * @return The joined string
   */
  public static String join(String separator, List<String> list) {
    StringBuffer sBuffer = new StringBuffer();
    Iterator<String> it = list.iterator();
    while(it.hasNext()) {
      Object listItemObj = it.next();
      String listItem = null;
      try {
        listItem = (String)listItemObj;
      }
      catch (ClassCastException e) {
        listItem = ((Integer)listItemObj).toString();
      }

      sBuffer.append(listItem);
      if (it.hasNext())
        sBuffer.append(separator);
    }
    
    return sBuffer.toString();
  }
}
